use super::Operation;
use crate::core::error::operation_error::OperationError;
use crate::core::step_context::StepContext;
use crate::core::{error::ParrotError, path_type::TypedPath};
use async_zip::error::ZipError;
use async_zip::tokio::read::fs;
use snafu::{ResultExt, Snafu};
use std::sync::Arc;
use tokio::{fs::File, io::copy};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tonic::async_trait;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum Error {
    #[snafu(display("Failed to read zip file at {path}: {source}"))]
    ReadZipFile { path: String, source: ZipError },

    #[snafu(display("Failed to create directory at {path}: {source}"))]
    CreateDestDir {
        path: String,
        source: std::io::Error,
    },
}

impl From<Error> for OperationError {
    fn from(value: Error) -> Self {
        OperationError::UnZip { source: value }
    }
}

pub struct Unzip {
    src: TypedPath,
    dst: TypedPath,
}

#[async_trait]
impl Operation for Unzip {
    async fn run(&mut self, context: Arc<StepContext>) -> Result<(), OperationError> {
        let src = match &self.src {
            TypedPath::Normal(p) => p.clone(),
            TypedPath::Contextual(s) => context.job_context().path().join(s),
        };

        let dst = match &self.dst {
            TypedPath::Normal(p) => p.clone(),
            TypedPath::Contextual(s) => context.job_context().path().join(s),
        };

        let zip = fs::ZipFileReader::new(&src)
            .await
            .context(ReadZipFileSnafu {
                path: &src.to_string_lossy().to_string(),
            })?;
        let entries = zip.file().entries().to_vec();

        tokio::fs::create_dir_all(&dst)
            .await
            .context(CreateDestDirSnafu {
                path: dst.to_string_lossy().to_string(),
            })?;

        for (i, e) in entries.into_iter().enumerate() {
            let file_name = e.filename().as_str().unwrap();
            let file_path = dst.join(file_name);
            context.log(format!("Extracting to {file_path:?}"));
            if e.dir().unwrap() {
                tokio::fs::create_dir_all(file_path).await.unwrap();
            } else {
                let reader = zip.reader_with_entry(i).await.unwrap();
                tokio::fs::create_dir_all(file_path.parent().unwrap())
                    .await
                    .unwrap();
                let mut dst_file = File::create(file_path).await.unwrap();
                copy(&mut reader.compat(), &mut dst_file).await.unwrap();
            }
        }

        Ok(())
    }
}

impl Unzip {
    pub fn new(src: TypedPath, dst: TypedPath) -> Self {
        Unzip { src, dst }
    }
}
